/**
 * 广寒宫
 * 网址:www.guanghangong.xyz
 */
package org.moon.framework.autoconfigure.log;

import com.google.common.collect.Maps;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.moon.framework.autoconfigure.MoonConstants;
import org.moon.framework.autoconfigure.log.annotation.OperateLog;
import org.moon.framework.autoconfigure.log.domain.OperateLogger;
import org.moon.framework.autoconfigure.secure.AuthUtils;
import org.moon.framework.autoconfigure.secure.domain.AuthInfo;
import org.moon.framework.autoconfigure.springmvc.response.R;
import org.moon.framework.autoconfigure.utils.Func;
import org.moon.framework.autoconfigure.utils.IpUtils;
import org.moon.framework.autoconfigure.utils.JsonUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.IntStream;

/**
 * 系统日志拦截器
 * @author moon
 */
@Aspect
@Configuration
@ConditionalOnProperty(value = "moon.log.operate-log.enabled",matchIfMissing=true)
@Slf4j
public class OperateLogAspect {

	@Value("${spring.application.name}")
	private String applicationName;

	@Value("${moon.log.operate-log.enabled:true}")
	private Boolean operateLogEnabled;

	@Autowired
	private RedisTemplate redisTemplate;

	/**
	 * 用于记录操作内容的上下文
	 */
	private static final ThreadLocal<String> CONTENT = new ThreadLocal<>();
	/**
	 * 用于记录拓展字段的上下文
	 */
	private static final ThreadLocal<Map<String, Object>> EXTS = new ThreadLocal<>();

	@Around("@annotation(org.moon.framework.autoconfigure.log.annotation.OperateLog)") // 兼容处理，只添加 @OperateLog 注解的情况
	public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
		OperateLog sysLog = getMethodAnnotation(joinPoint, OperateLog.class);
		ApiOperation apiOperation = getMethodAnnotation(joinPoint, ApiOperation.class);
		return around0(joinPoint, sysLog, apiOperation);
	}

	private Object around0(ProceedingJoinPoint joinPoint, OperateLog sysLog, ApiOperation apiOperation) throws Throwable {
		// 记录开始时间
		Date startTime = new Date();
		try {
			// 执行原有方法
			Object result = joinPoint.proceed();
			// 记录正常执行时的操作日志
			this.log(joinPoint, sysLog, apiOperation, startTime, result, null);
			return result;
		} catch (Throwable exception) {
			this.log(joinPoint, sysLog, apiOperation, startTime, null, exception);
			throw exception;
		} finally {
			clearThreadLocal();
		}
	}

	public static void setContent(String content) {
		CONTENT.set(content);
	}

	public static void addExt(String key, Object value) {
		if (EXTS.get() == null) {
			EXTS.set(new HashMap<>());
		}
		EXTS.get().put(key, value);
	}

	private static void clearThreadLocal() {
		CONTENT.remove();
		EXTS.remove();
	}

	private void log(ProceedingJoinPoint joinPoint, OperateLog sysLog, ApiOperation apiOperation,
					 Date startTime, Object result, Throwable exception) {
		try {
			// 判断不记录的情况
			if (!isLogEnable(joinPoint, sysLog)) {
				return;
			}
			// 真正记录操作日志
			OperateLogger sysOperLog = new OperateLogger();
			// 补全通用字段
			sysOperLog.setRequestFrom(AuthUtils.getRequestFrom());
			sysOperLog.setRequestId(AuthUtils.getRequestId());
			sysOperLog.setStartTime(startTime);
			sysOperLog.setApplicationName(applicationName);
			// 补充用户信息
			fillUserFields(sysOperLog);
			// 补全模块信息
			fillModuleFields(sysOperLog, joinPoint, sysLog, apiOperation);
			// 补全请求信息
			fillRequestFields(sysOperLog);
			// 补全方法信息
			fillMethodFields(sysOperLog, joinPoint, sysLog, startTime, result, exception);
			// 异步记录日志
			redisTemplate.opsForList().rightPush(MoonConstants.REDIS_KEY_OPERATE_LOG,sysOperLog);
		} catch (Throwable ex) {
			log.error("[log][记录操作日志时，发生异常，其中参数是 joinPoint({}) operateLog({}) apiOperation({}) result({}) exception({}) ]",
					joinPoint, sysLog, apiOperation, result, exception, ex);
		}
	}

	private void fillUserFields(OperateLogger operateLogger) {
		AuthInfo info = AuthUtils.getAuthInfo();
		if(info.getId()!=null){
			operateLogger.setNickName(info.getNickName());
			operateLogger.setCreateBy(info.getId());
			operateLogger.setCreateTime(new Date());
			operateLogger.setCreateDept(info.getDeptId());
			operateLogger.setTenantCode(info.getTenantCode());
			operateLogger.setUpdateBy(info.getId());
			operateLogger.setUpdateTime(new Date());
		}
	}

	private void fillModuleFields(OperateLogger sysOperLog,
										 ProceedingJoinPoint joinPoint, OperateLog sysLog, ApiOperation apiOperation) {
		// module 属性
		if (sysLog != null) {
			sysOperLog.setModule(sysLog.module());
		}
		if (Func.isEmpty(sysOperLog.getModule())) {
			Api api = getClassAnnotation(joinPoint, Api.class);
			if (api != null) {
				// 优先读取 @API 的 name 属性
				if (Func.isNotEmpty(api.value())) {
					sysOperLog.setModule(api.value());
				}
				// 没有的话，读取 @API 的 tags 属性
				if (Func.isEmpty(sysOperLog.getModule()) && Func.isNotEmpty(api.tags())) {
					sysOperLog.setModule(api.tags()[0]);
				}
			}
		}
		// name 属性
		if (sysLog != null) {
			sysOperLog.setName(sysLog.name());
		}
		if (Func.isEmpty(sysOperLog.getName()) && apiOperation != null) {
			sysOperLog.setName(apiOperation.value());
		}
		// content 和 exts 属性
		sysOperLog.setContent(CONTENT.get());
		sysOperLog.setExts(EXTS.get());
	}

	private void fillRequestFields(OperateLogger sysOperLog) {
		// 获得 Request 对象
		HttpServletRequest request = Func.getRequest();
		if (request == null) {
			return;
		}
		// 补全请求信息
		sysOperLog.setRequestMethod(request.getMethod());
		sysOperLog.setRequestUrl(request.getRequestURI());
		sysOperLog.setUserIp(IpUtils.getIpAddr());
		sysOperLog.setUserAgent(Func.getUserAgent());
	}

	private void fillMethodFields(OperateLogger sysOperLog,
										 ProceedingJoinPoint joinPoint, OperateLog sysLog,
										 Date startTime, Object result, Throwable exception) {
		MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
		sysOperLog.setJavaMethod(methodSignature.toString());
		if (sysLog == null || sysLog.logArgs()) {
			sysOperLog.setJavaMethodArgs(obtainMethodArgs(joinPoint));
		}
		if (sysLog == null || sysLog.logResultData()) {
			sysOperLog.setResultData(obtainResultData(result));
		}
		sysOperLog.setDuration((int) (System.currentTimeMillis() - startTime.getTime()));
		// （正常）处理 resultCode 和 resultMsg 字段
		if (result != null) {
			if (result instanceof R) {
				R<?> commonResult = (R<?>) result;
				sysOperLog.setResultCode(commonResult.getCode());
				sysOperLog.setResultMsg(commonResult.getMsg());
				sysOperLog.setResultData(obtainResultData(result));
			} else {
				sysOperLog.setResultCode(MoonConstants.SUCCESS.getCode());
			}
		}
		// （异常）处理 resultCode 和 resultMsg 字段
		if (exception != null) {
			sysOperLog.setResultCode(MoonConstants.FAIL.getCode());
			sysOperLog.setResultMsg(ExceptionUtils.getRootCauseMessage(exception));
		}
	}

	private boolean isLogEnable(ProceedingJoinPoint joinPoint, OperateLog sysLog) {
		//关闭记录
		if(!operateLogEnabled){
			return false;
		}
		// 有 @OperateLog 注解的情况下
		if (sysLog != null) {
			return sysLog.enable();
		}
		// 没有 @ApiOperation 注解的情况下，只记录 POST、PUT、DELETE 的情况
		return obtainFirstLogRequestMethod(obtainRequestMethod(joinPoint)) != null;
	}

	private static RequestMethod obtainFirstLogRequestMethod(RequestMethod[] requestMethods) {
		if (Func.isEmpty(requestMethods)) {
			return null;
		}
		return Arrays.stream(requestMethods).filter(requestMethod ->
						requestMethod == RequestMethod.POST
								|| requestMethod == RequestMethod.PUT
								|| requestMethod == RequestMethod.DELETE)
				.findFirst().orElse(null);
	}


	private static RequestMethod[] obtainRequestMethod(ProceedingJoinPoint joinPoint) {
		RequestMapping requestMapping = AnnotationUtils.getAnnotation( // 使用 Spring 的工具类，可以处理 @RequestMapping 别名注解
				((MethodSignature) joinPoint.getSignature()).getMethod(), RequestMapping.class);
		return requestMapping != null ? requestMapping.method() : new RequestMethod[]{};
	}

	@SuppressWarnings("SameParameterValue")
	private static <T extends Annotation> T getMethodAnnotation(ProceedingJoinPoint joinPoint, Class<T> annotationClass) {
		return ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(annotationClass);
	}

	@SuppressWarnings("SameParameterValue")
	private static <T extends Annotation> T getClassAnnotation(ProceedingJoinPoint joinPoint, Class<T> annotationClass) {
		return ((MethodSignature) joinPoint.getSignature()).getMethod().getDeclaringClass().getAnnotation(annotationClass);
	}

	private static String obtainMethodArgs(ProceedingJoinPoint joinPoint) {
		// TODO 提升：参数脱敏和忽略
		MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
		String[] argNames = methodSignature.getParameterNames();
		Object[] argValues = joinPoint.getArgs();
		// 拼接参数
		Map<String, Object> args = Maps.newHashMapWithExpectedSize(argValues.length);
		for (int i = 0; i < argNames.length; i++) {
			String argName = argNames[i];
			Object argValue = argValues[i];
			// 被忽略时，标记为 ignore 字符串，避免和 null 混在一起
			args.put(argName, !isIgnoreArgs(argValue) ? argValue : "[ignore]");
		}
		return JsonUtils.obj2string(args);
	}

	private static String obtainResultData(Object result) {
		// TODO 提升：结果脱敏和忽略
		if (result instanceof R) {
			result = ((R<?>) result).getData();
		}
		return JsonUtils.obj2string(result);
	}

	private static boolean isIgnoreArgs(Object object) {
		Class<?> clazz = object.getClass();
		// 处理数组的情况
		if (clazz.isArray()) {
			return IntStream.range(0, Array.getLength(object))
					.anyMatch(index -> isIgnoreArgs(Array.get(object, index)));
		}
		// 递归，处理数组、Collection、Map 的情况
		if (Collection.class.isAssignableFrom(clazz)) {
			return ((Collection<?>) object).stream()
					.anyMatch((Predicate<Object>) OperateLogAspect::isIgnoreArgs);
		}
		if (Map.class.isAssignableFrom(clazz)) {
			return isIgnoreArgs(((Map<?, ?>) object).values());
		}
		// obj
		return object instanceof MultipartFile
				|| object instanceof HttpServletRequest
				|| object instanceof HttpServletResponse
				|| object instanceof BindingResult;
	}
}